package data

import (
	"errors"
	"fmt"
	"gitee.com/Cauchy_AQ/gokv/fio"
	"hash/crc32"
	"io"
	"path/filepath"
)

var (
	ErrInvalidCRC = errors.New("invalid crc")
)

const (
	// DataFileSuffix 数据文件后缀名
	DataFileSuffix string = ".data"
	// HintFileName  索引文件名
	HintFileName string = "hint-index"
	// MergeFileFinished merge 完成文件
	MergeFileFinished string = "merge-finished"
	// SeqNoFileName 事务序列号文件
	SeqNoFileName = "seq-no"
)

// DataFile 数据文件
type DataFile struct {
	FileId    uint32 // 文件 id
	WriteOff  int64  // 文件写入偏移
	IOManager fio.IOManager
}

// OpenDataFile 打开新的数据文件
func OpenDataFile(dirPath string, fileId uint32, ioType fio.FileIOType) (*DataFile, error) {
	fileName := GetDataFileName(dirPath, fileId)
	return newDataFile(fileName, fileId, ioType)
}

// OpenHintFile 打开 Hint 索引文件
func OpenHintFile(dirPath string, ioType fio.FileIOType) (*DataFile, error) {
	fileName := filepath.Join(dirPath, HintFileName)
	return newDataFile(fileName, 0, ioType)
}

// OpenMergeFinishedFile 打开 merge 完成文件
func OpenMergeFinishedFile(dirPath string) (*DataFile, error) {
	fileName := filepath.Join(dirPath, MergeFileFinished)
	return newDataFile(fileName, 0, fio.StandardFIO)
}

// OpenSeqNoFile 打开事务序列号文件
func OpenSeqNoFile(dirPath string) (*DataFile, error) {
	fileName := filepath.Join(dirPath, SeqNoFileName)
	return newDataFile(fileName, 0, fio.StandardFIO)
}

// GetDataFileName 获取数据文件名称
func GetDataFileName(dirPath string, fileId uint32) string {
	fileName := filepath.Join(dirPath, fmt.Sprintf("%09d", fileId)+DataFileSuffix)
	return fileName
}

// newDataFile 新建 DataFile 文件
func newDataFile(fileName string, fileId uint32, ioType fio.FileIOType) (*DataFile, error) {
	ioManager, err := fio.NewIOManager(fileName, ioType)
	if err != nil {
		return nil, err
	}
	return &DataFile{
		FileId:    fileId,
		WriteOff:  0,
		IOManager: ioManager,
	}, nil
}

// ReadLogRecord 读取指定偏移位置的一条数据记录，返回 LogRecord 数据及其编码的字节长度，和错误信息
func (d *DataFile) ReadLogRecord(offset int64) (*LogRecord, int64, error) {
	// 获取文件的总大小
	fileSize, err := d.IOManager.Size()
	if err != nil {
		return nil, 0, err
	}

	// 如果读取头部信息超过了文件大小，那么读取到文件末尾即可
	// 避免最后一条数据没能读取成功
	var headerBytes int64 = maxLogRecordHeaderSize
	if headerBytes+offset > fileSize {
		headerBytes = fileSize - offset
	}
	// 读取header信息
	headerBuf, err := d.readNBytes(headerBytes, offset)
	if err != nil {
		return nil, 0, err
	}

	// 对header解码
	header, headerSize := decodeLogRecordHeader(headerBuf)
	// 读取到文件末尾
	if header == nil {
		return nil, 0, io.EOF
	}
	if header.crc == 0 && header.keySize == 0 && header.valueSize == 0 {
		return nil, 0, io.EOF
	}

	// 取出头部中 key 和 value 的长度信息
	keySize, valueSize := int64(header.keySize), int64(header.valueSize)
	// 计算数据记录长度
	logRecordSize := headerSize + keySize + valueSize

	// 构建数据结构
	logRecord := &LogRecord{
		Type: header.recordType,
	}
	// 读取实际 key/value
	if keySize > 0 || valueSize > 0 {
		kvBuf, err := d.readNBytes(keySize+valueSize, offset+headerSize)
		if err != nil {
			return nil, 0, err
		}
		logRecord.Key = kvBuf[:keySize]
		logRecord.Value = kvBuf[keySize:]
	}

	// 校验数据有效性，CRC校验
	crc := getLogRecordCRC(logRecord, headerBuf[crc32.Size:headerSize])
	if crc != header.crc {
		return nil, 0, ErrInvalidCRC
	}

	return logRecord, logRecordSize, nil
}

func (d *DataFile) Write(buf []byte) error {
	n, err := d.IOManager.Write(buf)
	d.WriteOff += int64(n)
	return err
}

// WriteHintRecord 写入索引信息到 Hint 文件中
func (d *DataFile) WriteHintRecord(key []byte, pos *LogRecordPos) error {
	record := &LogRecord{
		Key:   key,
		Value: EncodeLogRecordPos(pos),
	}
	encRecord, _ := EncodeLogRecord(record)
	return d.Write(encRecord)
}

func (d *DataFile) Sync() error {
	return d.IOManager.Sync()
}

func (d *DataFile) Close() error {
	return d.IOManager.Close()
}

func (d *DataFile) readNBytes(n int64, offset int64) ([]byte, error) {
	b := make([]byte, n)
	d.IOManager.Read(b, offset)
	return b, nil
}

// SetIOManager 重置文件的 IO 类型
func (d *DataFile) SetIOManager(dirPath string, ioType fio.FileIOType) error {
	if err := d.IOManager.Close(); err != nil {
		return err
	}
	ioManager, err := fio.NewIOManager(GetDataFileName(dirPath, d.FileId), ioType)
	if err != nil {
		return err
	}

	d.IOManager = ioManager
	return nil
}
